# module calendar

#################################
# Fonctions liées au calendrier #
#################################

# Ce module s'inspire de la version UNIX de ctime() (cette version étant aussi
# celle du standard C et celle du standard POSIX); il s'inspire également du
# reste du fichier d'en-tête time
# Quelques différences subtiles mais importantes :
# - l'ordre des arguments passés aux fonctions mktime et asctime diffère de 
#   l'ordre retenu par le standard C pour la définition de la structure de 
#   données 'struct tm', ceci pour plus de simplicité
# - les numéros des mois vont de 1 à 12, pas de 0 à 11; les tableaux contenant
#   des mois ont un élément 0 qui ne sert à rien
# - le lundi est le premier jour de la semaine (numéroté 0)

# Ces deux paramètres sont en fait ceux du module 'time' :
epoch = 1970		# Le temps a commencé le premier janvier de cette année 
                    # (00:00:00 UCT)
day_0 = 3		# "L'époque" commence un jeudi (lundi = 0)

# Renvoie 1 si on lui passe une année bissextile, 0 si on lui passe une année
# non-bissextile
def isleap(year):
	return year % 4 = 0 and (year % 100 <> 0 or year % 400 = 0)

# Quelques constantes représentant les mois auxquels on fera référence dans
# la suite
January = 1
February = 2

# Le nombre de jours par mois (sauf pour février lors des années bissextiles)
mdays = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)

# L'exception qui est déclenchée en cas d'entrée incorrecte (cette exception
# peut être associée à un paramètre qui explique plus précisément le problème)
error = 'erreur liée au calendrier'

# Transforme un nombre de secondes depuis l'époque en une date/heure précise
# du calendrier
def gmtime(secs):
	if secs < 0: raise error, 'gmtime() s\'est vu passée un nombre négatif'
	mins, secs = divmod(secs, 60)
	hours, mins = divmod(mins, 60)
	days, hours = divmod(hours, 24)
	wday = (days + day_0) % 7
	year = epoch
	# XXX La majeure partie de la boucle suivante pourrait être remplacée par
	# une division
	while 1:
		yd = 365 + isleap(year)
		if days < yd: break
		days = days - yd
		year = year + 1
	yday = days
	month = January
	while 1:
		md = mdays[month] + (month = February and isleap(year))
		if days < md: break
		days = days - md
		month = month + 1
	return year, month, days + 1, hours, mins, secs, yday, wday
	# XXX Renvoyer aussi le numéro de la semaine?

# Renvoie le nombre d'années bissextiles dans l'intervalle [y1;y2[
# Cette fonction suppose que y1 <= y2 et que les années spécifiées
# ne sont pas celles de siècles (comme 2100, 2200) qui ne sont pas
# bissextiles.
def leapdays(y1, y2):
	return (y2+3)/4 - (y1+3)/4

# L'inverse de gmtime() :
# Transforme une date/heure du calendrier (fuseau horaire : UCT; sans yday
# et wday) en un nombre de secondes depuis l'époque.
def mktime(year, month, day, hours, mins, secs):
	days = day - 1
	for m in range(January, month): days = days + mdays[m]
	if isleap(year) and month > February: days = days+1
	days = days + (year-epoch)*365 + leapdays(epoch, year)
	return ((days*24 + hours)*60 + mins)*60 + secs

# Noms complets et abrégés des jours de la semaine
day_name = ('Lundi', 'Mardi', 'Mercredi', 'Jeudi')
day_name = day_name + ('Vendredi', 'Samedi', 'Dimanche')
day_abbr = ('Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim')

# Noms complets et abrégés des mois (ces tableaux commencent à 1!!!)
month_name =          ('', 'Janvier',   'Février', 'Mars',    'Avril')
month_name = month_name + ('Mai',       'Juin',     'Juillet',     'Août')
month_name = month_name + ('Septembre', 'Octobre',  'Novembre', 'Décembre')
month_abbr =       ('   ', 'Janv', 'Févr', 'Mars', 'Avr', 'Mai', 'Juin')
month_abbr = month_abbr + ('Juil', 'Août', 'Sept', 'Oct', 'Nov', 'Déc')

# Comble une chaîne avec des zéros pour que celle-ci ait une
# longueur de 2 (cette fonction est une assistante de asctime())
def dd(s):
	while len(s) < 2: s = '0' + s
	return s

# Comble une chaîne avec des espaces pour que celle-ci ait une
# longueur de 2 (cette fonction est une assistante de asctime())
def zd(s):
	while len(s) < 2: s = ' ' + s
	return s

# Transforme une date/heure du calendrier (au même format que
# celles renvoyées par gmtime()) en une chaîne (le paramètre
# yday est présent pour des raisons de compatibilité avec gmtime())
def asctime(year, month, day, hours, mins, secs, yday, wday):
	s = day_abbr[wday] + ' ' + zd(`day`) + ' ' + month_abbr[month]
	s = s + ' ' + dd(`hours`) + ':' + dd(`mins`) + ':' + dd(`secs`)
	return s + ' ' + `year`

# Localisation : le nombre de minutes à l'ouest du méridien de Greenwich
# timezone = -2*60	# l'heure en Europe centrale, avec heure d'été
timezone = 5*60		# EST (pfff, je crois que time() ne renvoie pas d'heure UCT)

# La fonction localtime ignore pour l'instant les considérations liées à
# l'heure d'été -- on simule l'heure locale en ajustant l'heure avec timezone
def localtime(secs):
	return gmtime(secs - timezone*60)

# La fonction ctime, conforme à la version UNIX de ctime (sauf qu'en Python
# on n'ajoute pas de '\n' à la fin de la chaîne!)
def ctime(secs):
	return asctime(localtime(secs))

###############################################
# Ajouts qui ne sont pas dans la version UNIX #
###############################################

# Fonctions d'affichage du calendrier...

# Renvoie le jour de la semaine correspondant à une année (>=1970), un mois
# (entre 1 et 12) et un jour (entre 1 et 31); la fonction renvoie un nombre
# entre 0 et 6, qui représente un jour entre lundi et dimanche
def weekday(year, month, day):
	secs = mktime(year, month, day, 0, 0, 0)
	days = secs / (24*60*60)
	return (days + day_0) % 7

# Renvoie le jour de la semaine correspondant à une année et un mois, et le
# nombre de jours de ce mois; la fonction renvoie un nombre entre 0 et 6,
# qui représente un jour entre lundi et dimanche, et un nombre de jours
# entre 28 et 31
def monthrange(year, month):
	day1 = weekday(year, month, 1)
	ndays = mdays[month] + (month = February and isleap(year))
	return day1, ndays

# Renvoie une matrice représentant un mois du calendrier
# Chaque ligne représente une semaine; les jours se trouvant en
# dehors du mois ont la valeur zéro
def _monthcalendar(year, month):
	day1, ndays = monthrange(year, month)
	rows = []
	r7 = range(7)
	day = 1 - day1
	while day <= ndays:
		row = [0, 0, 0, 0, 0, 0, 0]
		for i in r7:
			if 1 <= day <= ndays: row[i] = day
			day = day + 1
		rows.append(row)
	return rows

# Une interface entre _monthcalendar et l'utilisateur qui met en cache les
# résultats
mc_cache = {}
def monthcalendar(year, month):
	key = `year` + month_abbr[month]
	try:
		return mc_cache[key]
	except RuntimeError:
		mc_cache[key] = ret = _monthcalendar(year, month)
		return ret

# Centre une chaîne au sein d'un champ
def center(str, width):
	n = width - len(str)
	if n < 0: return str
	return ' '*(n/2) + str + ' '*(n-n/2)

# XXX Dans la suite, les éléments passés à print sont séparés par des espaces!

# Affiche une semaine entière (sans passage à la ligne)
def prweek(week, width):
	for day in week:
		if day = 0: print ' '*width,
		else:
			if width > 2: print ' '*(width-3),
			if day < 10: print '',
			print day,

# Renvoie un en-tête pouvant servir lors de l'affichage d'une semaine
def weekheader(width):
	str = ''
	for i in range(7):
		if str: str = str + ' '
		str = str + day_abbr[i%7][:width]
	return str

# Affiche un mois du calendrier
def prmonth(year, month):
	print weekheader(3)
	for week in monthcalendar(year, month):
		prweek(week, 3)
		print

# L'espacement devant se trouver entre les colonnes de chaque mois
spacing = '    '

# Un format en 3 colonnes pour les calendriers couvrant une année entière
# 
def format3c(a, b, c):
	print center(a, 20), spacing, center(b, 20), spacing, center(c, 20)

# Affiche le calendrier d'une année
def prcal(year):
	header = weekheader(2)
	format3c('', `year`, '')
	for q in range(January, January+12, 3):
		print
		format3c(month_name[q], month_name[q+1], month_name[q+2])
		format3c(header, header, header)
		data = []
		height = 0
		for month in range(q, q+3):
			cal = monthcalendar(year, month)
			if len(cal) > height: height = len(cal)
			data.append(cal)
		for i in range(height):
			for cal in data:
				if i >= len(cal):
					print ' '*20,
				else:
					prweek(cal[i], 2)
				print spacing,
			print
